package com.example.mq.mqserver.datacenter;

import com.example.mq.common.BinaryTool;
import com.example.mq.common.MqException;
import com.example.mq.mqserver.core.MSGQueue;
import com.example.mq.mqserver.core.Message;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

/**
 * 用这个类管理文件中相关数据：
 * 主要有两个文件：queue_data.txt 和 queue_data_new.txt
 */
@Slf4j
public class MessageFileManage {

    public void init() {

    }

    //维护一个静态内部类Stat：表示文件之中总数和有效数
    public static class Stat {
        private int totalCount;
        private int validCount;

        public int getTotalCount() {
            return totalCount;
        }

        public void setTotalCount(int totalCount) {
            this.totalCount = totalCount;
        }

        public int getValidCount() {
            return validCount;
        }

        public void setValidCount(int validCount) {
            this.validCount = validCount;
        }
    }

    //获取消息队列的文件路径
    private String getQueueDir(String queueName) {
        return "./data/" + queueName;
    }

    //获取消息队列中消息的文件路径
    private String getQueueDataDir(String queueName) {
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    //获取消息中数据的文件路径
    //由于是一个二进制文件，采用删除文件中消息的方式是逻辑删除（并不是真正的删除）
    // 所以需要单独维护一个文件记录当前文件总消息数和有效消息数，以便在总消息数达到多少（拍脑门拍出来的），
    // 且有效消息数占到多少是触发回收（这里是复制算法，单独搞一个文件，把有效消息拷贝过去，然后直接删除掉之前文件中的所有消息）
    //上述垃圾回收的过程是比较消耗cpu资源的，所以不能频繁发生（具体情况具体分析）。
    //约定文件中内容格式为：总数\t有效数
    private String getQueueStatDir(String queueName) {
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

    //读取queue_data.txt文件中数据，并且赋值给stat
    private Stat readStat(String queueName) {
        Stat stat = new Stat();
        //直接使用Scanner进行读取就好
        try(InputStream inputStream = new FileInputStream(getQueueStatDir(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    //向queue_data.txt文件写入数据（stat）
    private void writeStat(String queueName,Stat stat) {
        try(OutputStream outputStream = new FileOutputStream(getQueueStatDir(queueName))) {
            PrintWriter writer = new PrintWriter(outputStream);
            writer.write(stat.totalCount + "\t" + stat.validCount);
            writer.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //创建队列目录和文件
    public void createQueueFiles(String queueName) throws IOException {
        //1.创建队列
        File queueDir = new File(getQueueDir(queueName));
        if(!queueDir.exists()) {
            //还有个上级目录：data目录，所以用mkdirs
            boolean ok = queueDir.mkdirs();
            if(!ok) {
                log.warn("创建文件失败");
                throw new IOException("[createQueueFiles] 创建队列失败，queueDir：" + queueDir.getAbsolutePath());
            }
        }
        //2.创建消息文件
        File queueDataDir = new File(getQueueDataDir(queueName));
        if(!queueDataDir.exists()) {
            boolean ok = queueDataDir.createNewFile();
            if(!ok) {
                log.warn("创建文件失败");
                throw new IOException("[createQueueFiles] 创建消息文件失败，queueDataDir：" + queueDataDir.getAbsolutePath());
            }
        }
        //3.创建消息统计文件
        File queueStatDir = new File(getQueueStatDir(queueName));
        if(!queueStatDir.exists()) {
            boolean ok = queueStatDir.createNewFile();
            if(!ok) {
                log.warn("创建文件失败");
                throw new IOException("[createQueueFiles] 创建消息统计文件失败，queueStatDir：" + queueStatDir.getAbsolutePath());
            }
        }
        //4.给消息统计文件赋初始值
        Stat stat = new Stat();
        stat.validCount = 0;
        stat.totalCount = 0;
        writeStat(queueName,stat);
        log.info("创建文件成功");
    }

    //删除队列对应目录和文件
    //如果队列删除了，那么队列中的文件都会被删除
    public void deleteQueue(String queueName) throws IOException {
        //先删除里面文件
        File queueDataFile = new File(getQueueDataDir(queueName));
        if(queueDataFile.exists()) {
            boolean ok = queueDataFile.delete();
            if(!ok) {
                log.warn("删除文件失败");
                throw new IOException("删除文件失败");
            }
        }

        File queueStatFile = new File(getQueueStatDir(queueName));
        if(queueStatFile.exists()) {
            boolean ok = queueStatFile.delete();
            if(!ok) {
                log.warn("删除文件失败");
                throw new IOException("删除文件失败");
            }
        }

        File queueDir = new File(getQueueDir(queueName));
        if(queueDir.exists()) {
            boolean ok = queueDir.delete();
            if(!ok) {
                log.warn("删除文件失败");
                throw new IOException("删除文件失败");
            }
        }
        log.info("删除文件成功");
    }

    //检查当前队列目录和文件是否存在
    private boolean checkFileExist(String queueName) {
        File queueDir = new File(getQueueDir(queueName));
        if(!queueDir.exists()) {
            return false;
        }
        File queueDataDir = new File(getQueueDataDir(queueName));
        if(!queueDataDir.exists()) {
            return false;
        }
        File queueStatDir = new File(getQueueStatDir(queueName));
        if(!queueStatDir.exists()) {
            return false;
        }
        return true;
    }

    //这个方法是将message写入queue中
    public void writeMessage(MSGQueue queue, Message message) throws IOException {
        //1.检查当前队列目录和文件是否存在
        if(!checkFileExist(queue.getName())) {
            throw new IOException("队列或队列中文件不存在，queueName：" + queue.getName());
        }
        //2.将message序列化成二进制数据
        byte[] messageBinary = BinaryTool.toBytes(message);
        //对当前队列进行加锁，防止在多线程环境下出现线程安全问题
        synchronized(queue) {
            //3.设置message中的offsetBeg和offsetEnd
            File queueDataFile = new File(getQueueDataDir(queue.getName()));
            message.setOffsetBeg(queueDataFile.length() + 4);
            message.setOffsetEnd(queueDataFile.length() + messageBinary.length + 4);
//            messageBinary = BinaryTool.toBytes(message);
            try(OutputStream outputStream = new FileOutputStream(getQueueDataDir(queue.getName()),true)) {
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    //约定以下面格式进行写入
                    //先写入当前bytes的长度
                    //再写入具体的bytes（二进制数据）
                    dataOutputStream.writeInt(messageBinary.length);
                    dataOutputStream.write(messageBinary);
                }
            }
            //4.更新统计文件中的数据
            Stat stat = readStat(queue.getName());
            //可能出现线程安全问题
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(),stat);

        }
    }
    //只是进行逻辑删除，触发垃圾回收时，才真正进行删除
    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException {
        if (!checkFileExist(queue.getName())) {
            throw new IOException("队列或队列中文件不存在，queueName：" + queue.getName());
        }
        synchronized (queue) {
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataDir(queue.getName()), "rw")) {
                //1.先从文件中把对应message的信息读取出来
                //构造一个bytes数组，长度为message的length
                byte[] bytes = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                //移动光标到offsetBeg为止
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(bytes);
                //2.反序列化成java对象
                Message message1 = (Message) BinaryTool.fromBytes(bytes);
                message1.setIsValid((byte) 0x0);
                //3.在序列化成二进制数据写入文件中
                //注意光标的位置
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(BinaryTool.toBytes(message1));
            }
            //可以看到上述一顿操作也只是改变了文件中的一个字节
            //4.更新统计文件中的数据
            Stat stat = readStat(queue.getName());
            if (stat.validCount > 0) {
                stat.validCount -= 1;
            }
            writeStat(queue.getName(), stat);
        }
    }

    //加载队列中的所有消息
    public LinkedList<Message> loadAllMessagesFromQueue(String queueName) throws ClassNotFoundException, IOException {
        LinkedList<Message> messages = new LinkedList<>();
        try(InputStream inputStream = new FileInputStream(getQueueDataDir(queueName))){
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                //通过下面一个while循环就可以将文件读完
                //到文件末尾会抛出EOFException异常
                //可以通过捕获该异常来表示已经读取到文件末尾
                long currentOffset = 0;
                while(true) {
                    //1.读取message的长度
                    int messageSize = dataInputStream.readInt();
                    //2.读取message的内容
                    byte[] buffer = new byte[messageSize];
                    if(buffer.length != messageSize) {
                        throw new MqException("[MessageLoad]文件读取有误,queueName:" + queueName);
                    }
                    dataInputStream.read(buffer);
                    //3.反序列化成message对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    //判断是否为有效数据
                    if(message.getIsValid() != 0x1) {
                        currentOffset += (4 + messageSize);
                        continue;
                    }
                    //4.设置当前message的偏移量
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    currentOffset += (4 + messageSize);
                    messages.add(message);
                }

            } catch (EOFException e) {
                //如果发生了该异常，说明文件已经读取完成
                //只需要捕获一下当前异常，然后说明一下读取完成
                log.info("文件读取完成");
            }
        }
        return messages;
    }

    //检查当前是否要进行GC
    //回收规则是：当总数达到2000，且有效数据个数超过总数的一半时，就触发GC回收
    private boolean checkGC(String queueName) {
        Stat stat = readStat(queueName);
        if(stat.totalCount > 2000 && stat.validCount * 2 > stat.totalCount) {
            return true;
        }
        return false;
    }

    //新文件位置
    private String getNewQueueDataDir(String queueName) {
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }
    //GC回收
    //思路：创建一个新文件（queue_data_new.txt），然后把旧文件中的有效数据拷贝到新文件中
    //     然后直接删除掉旧文件，然后将原来旧文件的名称给新文件（queue_data.txt）
    public void gc(MSGQueue queue) throws IOException, ClassNotFoundException {
        //线程安全问题需要注意
        synchronized (queue) {
            //由于gc是比较耗时的操作，所以可以统计一哈消耗的时间
            long begTime = System.currentTimeMillis();
            //1.准备好新文件
            File newFile = new File(getNewQueueDataDir(queue.getName()));
            if (newFile.exists()) {
                throw new MqException("gc过程中数据没有删除干净");
            }
            boolean ok = newFile.createNewFile();
            if (!ok) {
                throw new MqException("新文件创建失败");
            }
            //2.将文件里的有效数据个数拷贝到新文件中
            List<Message> messages = loadAllMessagesFromQueue(queue.getName());
            try (OutputStream outputStream = new FileOutputStream(getNewQueueDataDir(queue.getName()),true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        if (message.getIsValid() == 0x1) {
                            byte[] buffer = BinaryTool.toBytes(message);
                            dataOutputStream.writeInt(buffer.length);
                            dataOutputStream.write(buffer);
                        }
                    }
                }
            }
            //3.删除旧文件queue_data.txt
            File oldFile = new File(getQueueDataDir(queue.getName()));
            if (oldFile.exists()) {
                boolean ok1 = oldFile.delete();
                if (!ok1) {
                    throw new MqException("删除queue_data.txt文件失败");
                }
            }
            //4.将新文件重新命名为queue_data.txt
            boolean ok2 = newFile.renameTo(oldFile);
            if (!ok2) {
                throw new MqException("重命名queue_data_new.txt文件为queue_data.txt失败");
            }
            //5.更新queue_stat.txt文件中的信息
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queue.getName(), stat);

            long endTime = System.currentTimeMillis();
            log.info("gc消耗的时间：" + (endTime - begTime));
        }
    }
}

